qutebrowser Completer
qutebrowser 作为一个命令驱动的浏览器,良好的补全体验非常关键。在 qutebrowser 中,补全模块的实现比较复杂,即包含 GUI 视图绘制,也包含补全逻辑。因此 qutebrowser 对视图、逻辑进行了拆分,Completer 专门负责补全模块的纯逻辑部分的实现。
_partition
方法的主要目的是将命令行文本分割成多个部分,并围绕光标位置进行分割。
这么说有些抽象,结合使用来看。当用户在命令模式下输入命令,此时是 Completer 介入的时机,为用户提供补全提示。
原理分析
以以下命令输入为例:
其中包含两部分内容:
- 命令内容:即
:open -t mmm
- 光标位置:用户可以移动光标,进行行内编辑
回到 _partition
,它的作用是根据命令内容和光标位置,对用户输入进行切分。
在我打字输入过程中,_partition
输入输出如下:
用户操作 | 方法输入(切分后) | 方法三元组输出 |
---|---|---|
输入 o |
['o'] |
[] 'o' [] |
输入 p |
['op'] |
[] 'op' [] |
输入 e |
['ope'] |
[] 'ope' [] |
输入 n |
['open'] |
[] 'open' [] |
输入 |
['open', ' '] |
['open'] '' [] |
输入 - |
['open', ' -'] |
['open'] '-' [] |
输入 t |
['open', ' -t'] |
['open'] '-t' [] |
输入 |
['open', ' -t', ' '] |
['open', '-t'] '' [] |
输入 m |
['open', ' -t', ' m'] |
['open', '-t'] 'm' [] |
输入 m |
['open', ' -t', ' mm'] |
['open', '-t'] 'mm' [] |
输入 m |
['open', ' -t', ' mmm'] |
['open', '-t'] 'mmm' [] |
左方向键 | ['open', ' -t', ' mmm'] |
['open', '-t'] 'mmm' [] |
左方向键 | ['open', ' -t', ' mmm'] |
['open', '-t'] 'mmm' [] |
左方向键 | ['open', ' -t', ' mmm'] |
['open', '-t'] 'mmm' [] |
左方向键 | ['open', ' -t', ' mmm'] |
['open'] '-t' ['mmm'] |
左方向键 | ['open', ' -t', ' mmm'] |
['open'] '-t' ['mmm'] |
以上过程,包含逐字输入,输入完成后,左移光标的过程。
_partition
返回一个元组,包含三个元素:光标前的部分,光标下的部分,光标后的部分。
结合拆解推演过程,对这个结果有了直观理解。
在函数实现上,有几个细节值得关注:
首先,在命令切分上,首先尝试使用 CommandParser 执行切分,如果命令不存在,则降级为 split 分割:
# 使用 CommandParser 解析文本,如果命令不存在,则降级为 split 分割
try:
parse_result = parser.CommandParser().parse(text, keep=True)
except cmdexc.NoSuchCommandError:
cmdline = split.split(text, keep=True)
else:
cmdline = parse_result.cmdline
_partition
的最核心逻辑,是计算当前光标落在哪个命令部分上,具体实现为:获取当前光标据开头的总距离,从头开始,以每个命令长度削减总距离,如果在某个部分总长度被减为负数,这个部分变为当前部分。之前、之后部分基于此便可获得。具体实现:
# 获取光标位置,并确保光标位置不超过文本长度
pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars
# ……
# 它遍历 parts 中的每一部分
for i, part in enumerate(parts):
# 消耗总长度
pos -= len(part)
# 找到当前部分,计算前后部分
if pos <= 0:
if part[pos-1:pos+1].isspace():
# cursor is in a space between two existing words
parts.insert(i, '')
prefix = [x.strip() for x in parts[:i]]
center = parts[i].strip()
# strip trailing whitespace included as a separate token
postfix = [x.strip() for x in parts[i+1:] if not x.isspace()]
return prefix, center, postfix
raise utils.Unreachable(f"Not all parts consumed: {parts}")
何处被使用
Command(输入命令的 EditText 组件)cursorPositionChanged 信号和 textChanged
- Command.update_completion 信号
- Completer.schedule_completion_update
- 通过
Completer._timer
触发异步Completer_update_completion
Completer._partition
- 通过
- Completer.schedule_completion_update
还有其他调用路径,略。
_get_new_completion
主要目的是根据当前的命令文本获取一个新的补全函数。它接收两个参数:光标前的命令块(before_cursor
)和光标下的命令块(under_cursor
)。
还是以 :open -t mmm
为例,当我在逐字录入时,主观感受:
- 首先通过
:
进入命令模式,此时补全界面列出所有命令 - 在我输入
o
、p
过程中,补全界面不断缩小范围 - 当我输入完成
open
后,补全界面变了,原本是补全命令,变成了补全 Url,给出的补全提示是我近期访问的 url 列表
这个交互体验细节非常酷。
代码实现如下:
def _get_new_completion(self, before_cursor, under_cursor):
# ...
if not before_cursor:
log.completion.debug('Starting command completion')
print('Starting command completion')
# 首次进入命令模式,列出所有命令的补全
return miscmodels.command
# 尝试根据命令获取输入
try:
cmd = objects.commands[before_cursor[0]]
except KeyError:
log.completion.debug("No completion for unknown command: {}"
.format(before_cursor[0]))
return None
# ...
argpos = len(before_cursor) - 1
try:
# 根据命令参数类型,获取对应的补全函数
func = cmd.get_pos_arg_info(argpos).completion
except IndexError:
log.completion.debug("No completion in position {}".format(argpos))
return None
# 返回新的补全函数
return func
objects.commands
(参见 qutebrowser objects) 中包含了 qutebrowser 中所有命令
本文作者:Maeiee
版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!
喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!